/**
 * Copyright 2007-2008 Michele Mostarda ( michele.mostarda@gmail.com ).
 * All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package org.asemantics.anthill;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.asemantics.anthill.chatconsole.ChatCommandLine;
import org.asemantics.anthill.chatconsole.ChatCommandListener;
import org.asemantics.anthill.chatconsole.TargetsCommandsProvider;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This task open a connection on a given server with a given context (a user)
 * and wait for {@link org.asemantics.anthill.CommandMessage}. Every time a command
 * message is received, the command parameters are setted as properites int the
 * current {@link org.apache.tools.ant.Project} environment, then the task corresponding
 * with the CommndMessage name is invoked.
 * After this a {@link CommandResponseMessage} is sent.
 * Such response message contain some properties colleted after the execution of the task and eventually
 * any raised exception.
 *
 * //TODO: return property list should be defined by the commander.
 *
 * <pre>
 *          <listener
 *			    server="serverhost"                       <-- XMPP listener server.
 *              port="5222"                               <-- Server port.
 *              security="enabled"                        <-- Security level.
 *			    username="user1@host1.com"                <-- listener username identity.
 *			    password="******"                         <-- listener password.
 *			    debugger="false"                          <-- debugger enabling.
 *			    compression="false"                       <-- compression enabling.
 *           >
 *           <chatconsole>                                <-- chat console node contains a list of allowed / denied users.
 *               <allow context="user@host"/>
 *           </chatconsole>
 *
 *           <returnproperty name="returnproperty1"/>     <-- properties to return for every invocation.
 *           <returnproperty name="returnproperty2"/>
 *           <returnproperty name="returnproperty3"/>
 *
 *       </listener>
 *
 * </pre>
 *
 * @author Michele Mostarda
 */
@SuppressWarnings({"BooleanMethodIsAlwaysInverted"})
public class CommandListenerTask extends ConnectedTaskBase {

    /**
     * Filters every package that is a {@link Message}.
     * //TODO: Handle message queue !
     */
    protected final class MessageProcessor implements PacketListener {

        /**
         * The chat command line used to manage this listener.
         */
        private final ChatCommandLine chatCommandLine;

        /**
         * This map contains a mapping between a user and the chat to this user.
         */
        private final Map<String,Chat> chats;

        /**
         * Constructor.
         */
        protected MessageProcessor() {
            chatCommandLine = new ChatCommandLine(getTargetsCommandsProvider());
            chats = new HashMap<String,Chat>();
        }

        public void processPacket(Packet packet) {
            if( ! (packet instanceof Message) ) {
                log(
                        "ChatConsolePacketProcessor: rejected packet: " +
                            (
                                    packet != null
                                ?
                                    packet.toXML()
                                :
                                    "<null>"
                            ),
                        Project.MSG_DEBUG
                );
                return;
            }

            final Message message = (Message) packet;
            final String sender = message.getFrom();
            if(sender == null || sender.trim().length() == 0) {
                throw new IllegalArgumentException("Invalid message sender");
            }

            // Open chat with the user.
            Chat chat;
            try {
                chat = getChatForUser(sender);
            } catch (Exception e) {
                String msg = "Exception while opening chat to the user '" + sender + "'";
                log(msg + ": " + e.getMessage(), Project.MSG_INFO);
                log(e.toString(), Project.MSG_WARN);
                throw new RuntimeException(e);
            }

            // Check if received an ant-generated message.
            if( message.getPropertyNames().contains(CommandMessage.COMMAND_TYPE) ) {
                // Processed as an automatic message.
                log("Message processed as an Ant-generated message", Project.MSG_DEBUG);

                CommandResponseMessage responseMessage;
                try {
                    CommandMessage  commandMessage  = CommandMessage.deserializeCommandMessage(message);
                    RemoteOperation remoteOperation = RemoteOperation.deserializeRemoteOperation(message);
                    responseMessage = remoteOperation.processCommand(CommandListenerTask.this, commandMessage);
                } catch (Throwable t) {
                    /*
                       Any exception occurring during deserialization and execution of command
                       that is not handled by the command itself.
                    */
                    responseMessage = new ExceptionCommandResponseMessage(t);
                }

                try {
                    chat.sendMessage(responseMessage);
                    log("sent response message " + responseMessage, Project.MSG_INFO);
                    log(responseMessage.toXML(), Project.MSG_DEBUG);
                } catch (XMPPException xmppe) {
                    log("Exception while sending message: " + xmppe.getMessage(), Project.MSG_INFO);
                    log(xmppe.toString(), Project.MSG_WARN);
                    throw new RuntimeException(xmppe);
                }
                
            } else {
                // Processed as a chat message.
                log("Message processed as a Chat message", Project.MSG_DEBUG);

                // Check chat console enabled.
                if(chatConsole == null) {
                    final String msg = "Chat console DISABLED on this listener";
                    log("Sending message: " + msg, Project.MSG_DEBUG);
                    try {
                        chat.sendMessage(msg);
                    } catch (XMPPException e) {
                        log("Error while sending message: " + msg, Project.MSG_WARN);
                    } finally {
                        return;
                    }
                }

                // Check sender authorization.
                String error = null;
                if( ! chatConsole.isAllowed(sender) ) {
                    error = "user '" + sender + "' is not allowed to use Chat Console";
                } else if( chatConsole.isDenied(sender) ) {
                    error = "user '" + sender + "' is denied to use Chat Console";
                }
                if (error != null) {
                    try {
                        chat.sendMessage(error);
                    } catch (XMPPException e) {
                        log("Error while sending message: " + error, Project.MSG_WARN);
                    } finally {
                        return;
                    }
                }

                // Process Chat Console command.
                ChatCommandListener chatCommandListener = new ChatCommandListener( CommandListenerTask.this, chatCommandLine );
                chatCommandListener.processMessage(chat, message);
            }
        }

        /**
         * Returns a chat for a given user identifier.
         * 
         * @param userId
         * @return
         */
        protected Chat getChatForUser(String userId) {
            Chat chat = chats.get(userId);
                if(chat == null) {
                    chat = getConnection().getChatManager().createChat(userId, null);
                    chats.put(userId, chat);
                }
            return chat;
        }

        /**
         * Disposes all the opened chats.
         */
        protected void disposeChats() {
            chats.clear();
        }
    }

    /**
     * Defines a return property node.
     * A return property is a property read after the execution of a command task and sent
     * into the {@link CommandResponseMessage}.
     */
    protected class ReturnProperty {

        /**
         * The name of the property.
         */
        private String name;

        /**
         * Returns the name of the property.
         *
         * @return
         */
        public String getName() {
            return name;
        }

        /**
         * Sets the name of the property.
         *
         * @param name
         */
        public void setName(String name) {
            validateAttribute(name, "name");
            this.name = name;
        }
    }

    /**
     * This node allows to enable and configure the listener <i>Chat Console</i>.
     *
     * @see org.asemantics.anthill.CommandListenerTask.ChatConsole
     */
    protected class ChatConsole {

        /**
         * Defines an allowed context node entry.
         */
        protected class Allow {

            /**
             * The allowed context identifier.
             */
            private String context;

            /**
             * Constructor.
             */
            public Allow() {}

            /**
             * Getter for context.
             *
             * @return
             */
            public String getContext() {
                return context;
            }

            /**
             * Setter for context.
             *
             * @param context the <i>context</i> cannot be null or empty.
             */
            public void setContext(String context) {
                validateAttribute(context, "context");
                this.context = context;
            }

        }

        /**
         * Defines a deny context node entry.
         */
        public class Deny {

            /**
             * The context to deny.
             */
            private String context;

            /**
             * Constructor.
             */
            public Deny() {}

            /**
             * Returns the deny context.
             *
             * @return
             */
            public String getContext() {
                return context;
            }

            /**
             * Sets the deny context.
             *
             * @param context
             */
            public void setContext(String context) {
                validateAttribute(context, "context");
                this.context = context;
            }
        }


        /**
         * The list of the contexts that are allowed to open a chat console
         * with this <i>Command Listener</i>.
         */
        private final List<Allow> allowed;

        /**
         * The list of context that are denied to open a chat console
         * with this <i>CommandListener</i>.
         */
        private final List<Deny> denied;

        /**
         * Constructor.
         */
        public ChatConsole() {
            allowed = new ArrayList<Allow>();
            denied  = new ArrayList<Deny> ();
        }

        /**
         * Creates an Allow node. This method signature is compliant with
         * the create* pattern expected by Ant to instantiate internal nodes.
         * 
         * @return
         */
        public Allow createAllow() {
            if( ! denied.isEmpty() ) {
                throw new BuildException("Cannot define both allowed and denied entries");
            }

            Allow allow = new Allow();
            allowed.add(allow);
            return allow;
        }

        /**
         * Creates an Deny node. This method signature is compliant with
         * the create* pattern expected by Ant to instantiate internal nodes.
         *
         * @return
         */
        public Deny createDeny() {
            if( ! allowed.isEmpty() ) {
                throw new BuildException("Cannot define both allowd and denied entries");
            }

            Deny deny = new Deny();
            denied.add(deny);
            return deny;
        }

        /**
         * Returns <code>true</code> if a <i>context</i> is allowed, <code>false</code>
         * otherwise.
         * 
         * @param context
         * @return
         */
        public boolean isAllowed(String context) {
            if( allowed.isEmpty() ) {
                return true;
            }
            
            for(Allow allow : allowed) {
                if( allow.getContext().equals(context) ) {
                    return true;
                }
            }
            return false;
        }

        /**
         * Returns <code>true</code> if a <i>context</i> is denied, <code>false</code>
         * otherwise.
         *
         * @param context
         * @return
         */
        public boolean isDenied(String context) {
            if( denied.isEmpty() ) {
                return false;
            }

            for(Deny deny : denied) {
                if( deny.getContext().equals(context) ) {
                    return true;
                }
            }
            return false;
        }

    }

    /* BEGIN: node parameters. */

     /* Currenly empty... */

    /* END: node parameters. */

    /**
     * The <i>Presence</i> main object.
     * The presence is used to verify at startup that other listeners
     * with the same <i>context</i> are not already connected to the <i>server</i>.
     */
    private Presence presence;

    /**
     * Contans the list of the return properties defined in the CommandListenerTask node.
     */
    private final List<ReturnProperty> returnProperties;

    /**
     * The <i>ChatConsole</i> node single instance.
     */
    private ChatConsole chatConsole;

    /**
     * The common targets command provider instance.
     */
    private TargetsCommandsProvider targetsCommandsProvider;

    /**
     * The main cycle running flag.
     */
    private boolean running = true;


    /**
     * Constructor.
     */
    public CommandListenerTask() {
        returnProperties = new ArrayList();
    }

    /**
     * Creates a return property, respecting the <i>Ant</i>
     * pattern to define nested nodes of a {@link org.apache.tools.ant.Task}
     * 
     * @return a return property node.
     */
    public ReturnProperty createReturnProperty() {
        ReturnProperty returnProperty = new ReturnProperty();
        returnProperties.add(returnProperty);
        return returnProperty;
    }

    /**
     * Creates a <i>Chat Console</i> inner node, respecting the <i>Ant</i>
     * pattern to define nester nodes of a {@link org.apache.tools.ant.Task}.
     *
     * @return a Chat Console node.
     */
    public ChatConsole createChatConsole() {
        if(chatConsole != null) {
            throw new BuildException("Only a single chatConsole node can be specified.");
        }
        chatConsole = new ChatConsole();
        return chatConsole;
    }

    private final Object execute_synchronizer = new Object();

    /**
     * Executes the CommandListenerTask after loaded the configuration parameters.
     *
     * @throws BuildException
     */
    public synchronized void execute() throws BuildException {
        super.execute();

        // Message listener cycle.
        try {

            MessageProcessor messageProcessor = new MessageProcessor();
            XMPPConnection conn = getConnection();
            conn.addPacketListener(messageProcessor, null);

            synchronized (execute_synchronizer) {
                try {
                    execute_synchronizer.wait();
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                }
            }

        } finally {
            disconnect();
        }
    }

    /**
     * Controls the exit from the main cycle.
     */
    protected void exitCycle() {
        synchronized (execute_synchronizer) {
            log("end received", Project.MSG_INFO);
            execute_synchronizer.notifyAll();
        }
        //running = false;
        //interruptWaiting();
    }

    /**
     * Overrides the ConnectedTaskBase#getConnection method enabling the visibility of the
     * context by sending a <i>Present</i> packet.
     *
     * @return
     */
    protected XMPPConnection getConnection() {
        XMPPConnection conn = super.getConnection();
        loginIfNeeded();
        if( presence == null ) {
            log("enabling presence...", Project.MSG_INFO);
            presence = new Presence(Presence.Type.available);
            presence.setStatus("available_robot");
            conn.sendPacket(presence);
            log("done", Project.MSG_INFO);
        }
        return conn;
    }

    /**
     * Returns the targets command provider common instance.
     * 
     * @return
     */
    protected TargetsCommandsProvider getTargetsCommandsProvider() {
        if(targetsCommandsProvider == null) {
            targetsCommandsProvider = new TargetsCommandsProvider( getProject() );
        }
        return targetsCommandsProvider;
    }

    /**
     * Collects the properties declared in the returnProperties list
     * from the local {@link org.apache.tools.ant.Project} environment and
     * put it in a map.
     *
     * @return
     */
    protected Map<String,Object> collectProperties() {
        Map properties = new HashMap();
        Project project = getProject();
        for(ReturnProperty returnProperty : returnProperties) {
            properties.put(returnProperty.getName(), project.getProperty(returnProperty.getName()));
        }
        return properties;
    }

    /**
     * Activates the {@link org.asemantics.anthill.chatconsole.ChatConsoleOutputStream}
     * for this <i>CommandListenerTask</i>.
     *
     * @param chat the _chat on which activate the _chat console.
     * @return  the {@link org.asemantics.anthill.chatconsole.ChatCommandListener} driving the ChatConsole.
     */
    protected ChatCommandListener activateChatConsole(Chat chat) {
        TargetsCommandsProvider targetsCommandsProvider = new TargetsCommandsProvider( getProject() );
        ChatCommandLine chatCommandLine = new ChatCommandLine( targetsCommandsProvider );
        ChatCommandListener chatCommandListener = new ChatCommandListener( this, chatCommandLine );
        chat.addMessageListener( chatCommandListener );
        return chatCommandListener;
    }

    /**
     * Deactivate the {@link org.asemantics.anthill.chatconsole.ChatConsoleOutputStream} for the given _chat
     * deriven by <i>_chatCommandListener</i>.
     *
     * @param chat
     * @param chatCommandListener
     */
    protected void deactivateChatConsole(Chat chat, ChatCommandListener chatCommandListener) {
        chat.removeMessageListener(chatCommandListener);
    }

}
